﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Sockets;

namespace Upreader.Usenet.Nntp
{
    public class NntpClient : INntpClient
    {
        #region Configuration

        public const int DefaultPort = 119;
        public static readonly Encoding DefaultTextEncoding = Encoding.GetEncoding("iso-8859-1");

        #endregion

        #region Fields

        private readonly StreamChainBuilder _readStreamChainBuilder;
        private readonly StreamChainBuilder _writeStreamChainBuilder;

        private int _connectionTimeout;
        private string _address;
        private int _port;
        private bool _isSecuredConnection;
        private NntpClientAuthentication _authentication;
        private Encoding _textEncoding;

        private bool _isPostingAllowed;

        private NntpClientStatistics _statistics;

        #endregion // Fields

        #region Protected Properties

        #endregion // Protected Properties

        #region Properties

        /// <summary>
        /// Get the streamChainBuilder for read operations
        /// </summary>
        public StreamChainBuilder ReadStreamChainBuilder { get { return _readStreamChainBuilder; } }

        /// <summary>
        /// Get the streamChainBuilder for write operations
        /// </summary>
        public StreamChainBuilder WriteStreamChainBuilder { get { return _writeStreamChainBuilder; } }

        /// <summary>
        /// Get the underlying socket when the client is open
        /// </summary>
        public INntpSocket Socket { get; protected set; }

        /// <summary>
        /// Get statistics for this client
        /// </summary>
        public NntpClientStatistics Statistics { get { return _statistics; } }

        /// <summary>
        /// Get or set a timeout for the connection
        /// </summary>
        public int ConnectionTimeout
        {
            get { return _connectionTimeout; }
            set 
            {
                // connection must be closed in order to make any change to the connetion timeout
                ValidateIsClosed();

                if (value < 0)
                    throw new ArgumentException(Resource.NntpSocket_InvalidTimeOut);

                _connectionTimeout = value; 
            }
        }

        /// <summary>
        /// Get or set the address of the server as a string
        /// </summary>
        public string Address
        {
            get { return _address; }
            set
            {
                // connection must be closed when settingthis value
                ValidateIsClosed();

                _address = value;
            }
        }
            
        /// <summary>
        /// Get or set the port of the server to connect to
        /// </summary>
        public int Port
        {
            get { return _port; }
            set
            {
                // connection must be closed when setting this value
                ValidateIsClosed();

                if (value <= 0)
                    throw new ArgumentException(Resource.NntpSocket_InvalidPort, "port");

                _port = value;
            }
        }

        /// <summary>
        /// Get or set wether the connection is a secured connection
        /// </summary>
        public bool IsSecuredConnection
        {
            get { return _isSecuredConnection; }
            set
            {
                // connection must be closed when setting this value
                ValidateIsClosed();

                _isSecuredConnection = value;
            }
        }


        /// <summary>
        /// Get or set authentication info for the socket. When setting to null, no authentication is used
        /// </summary>
        public NntpClientAuthentication Authentication 
        {
            get { return _authentication; }
            set
            {
                // connection must be closed when setting this value
                ValidateIsClosed();

                _authentication = value;
            }
        }

        /// <summary>
        /// Get wether the socket is connected
        /// </summary>
        public virtual bool IsOpen 
        { 
            get
            {
                INntpSocket socket = Socket;

                return socket != null && socket.IsConnected;
            }
        }


        /// <summary>
        /// Get or set wether posting is allowed on the current connection
        /// </summary>
        public bool IsPostingAllowed
        {
            get { return _isPostingAllowed; }
            set
            {
                ValidateIsOpen();

                _isPostingAllowed = value;
            }
        }

        /// <summary>
        /// Get or set the encoding which this client should use for encoding text 
        /// </summary>
        public Encoding TextEncoding
        {
            get { return _textEncoding; }
            set
            {
                ValidateIsClosed();

                if (value == null)
                    throw new InvalidOperationException(Resource.NntpClient_TextEncodingIsNull);

                _textEncoding = value;
            }
        }

        #endregion // Properties

        #region Constructor

        public NntpClient()
        {
            Port = DefaultPort;
            TextEncoding = DefaultTextEncoding;

            _readStreamChainBuilder = new StreamChainBuilder();
            _writeStreamChainBuilder = new StreamChainBuilder();

            _statistics = new NntpClientStatistics();

            // register a nntp stream to deal with double dot problems
            _readStreamChainBuilder.Add(new StreamChainBuilderEntry(StreamChainBuilder.MutiblePriority, CreateNntpStream));

            // register streams for updating statistics
            _readStreamChainBuilder.Add(new StreamChainBuilderEntry(StreamChainBuilder.ImmutablePriority, CreateStatisticsStream));
            _writeStreamChainBuilder.Add(new StreamChainBuilderEntry(StreamChainBuilder.ImmutablePriority, CreateStatisticsStream));
        }

        private Stream CreateNntpStream(Stream baseStream)
        {
            return new Encodings.NntpStream(baseStream);
        }

        private Stream CreateStatisticsStream(Stream baseStream)
        {
            return new StatisticsStream(baseStream, (INntpStatisticsSource)_statistics);
        }

        #endregion // Constructor

        #region Methods

        /// <summary>
        /// Opens a connection
        /// </summary>
        public virtual void Open()
        {
            // make sure were closed
            ValidateIsClosed();

            // validate properties
            if (Address == null)
                throw new InvalidOperationException(Resource.NntpClient_AddressNotSpecified);

            // create the actual socket
            Socket = CreateSocket();

            Connect();

            if (Authentication != null)
            {
                Authenticate();
            }
        }

        /// <summary>
        /// Closes the connection and all underlying resources
        /// </summary>
        public virtual void Close()
        {
            // Make sure were open
            ValidateIsOpen();

            Disconnect();
            Socket = null;
        }

        public NewsGroup SelectNewsGroup(string group)
        {
            if (group == null)
                throw new ArgumentNullException("group");

            if (string.IsNullOrEmpty(group))
                throw new ArgumentException("group");

            WriteCommand("GROUP " + group);

            ResponseMessage message = ReadResponseMessage();
            Rfc977ResponseCode responseCode = (Rfc977ResponseCode)message.ParseResponseCode();

            if (responseCode == Rfc977ResponseCode.NewsgroupSelected)
            {
                string[] arguments = message.ParseArguments(4).ToArray();

                NewsGroup newsGroup = new NewsGroup(arguments[0],
                    Convert.ToInt32(arguments[1]),
                    Convert.ToInt32(arguments[2]),
                    Convert.ToInt32(arguments[3]));

                return newsGroup;
            }
            else if (responseCode == Rfc977ResponseCode.NoSuchNewsgroup)
            {
                throw new NntpGroupNotSelectedException();
            }
            else
            {
                throw new NntpException(message);
            }
        }

        public Stream GetArticleBodyStream(string articleId)
        {
            if (articleId == null)
                throw new ArgumentNullException("articleId");

            ValidateIsOpen();

            string command = string.Format("BODY <{0}>", articleId);

            WriteCommand(command);

            ResponseMessage message = ReadResponseMessage();
            Rfc977ResponseCode responseCode = (Rfc977ResponseCode)message.ParseResponseCode();

            Stream responseStream;

            switch (responseCode)
            {
                case Rfc977ResponseCode.ArticleRetrievedBodyFollows:
                    responseStream = BuildReadStreamChain();
                    break;
                case Rfc977ResponseCode.NoNewsgroupSelected:
                    throw new NntpGroupNotSelectedException();
                default:
                    throw new NntpException(message);
            }

            return responseStream;
        }

        public string GetHelpText()
        {
            ValidateIsOpen();

            WriteCommand("HELP");

            ResponseMessage message = ReadResponseMessage();
            Rfc977ResponseCode responseCode = (Rfc977ResponseCode)message.ParseResponseCode();

            switch (responseCode)
            {
                case Rfc977ResponseCode.HelpTextFollows:
                    return ReadString();
                default:
                    throw new NntpException(message);
            }
        }

        #endregion // Methods

        #region Protected Methods

        /// <summary>
        /// Writes the command specified to the socket
        /// </summary>
        /// <param name="command"></param>
        protected virtual void WriteCommand(string command)
        {
            using (Stream stream = BuildWriteStreamChain())
            using (StreamWriter writer = new StreamWriter(stream, TextEncoding))
            {
                writer.WriteLine(command);
            }
        }

        /// <summary>
        /// Reads the first line and interprets it as a response message
        /// </summary>
        protected virtual ResponseMessage ReadResponseMessage()
        {
            using (Stream stream = BuildReadStreamChain())
            {
                NntpStreamReader reader = new NntpStreamReader(stream, TextEncoding, 1);
                string line = reader.ReadLine();

                ResponseMessage message = new ResponseMessage(line);
                return message;
            }
        }

        /// <summary>
        /// Creates a underlying socket used when Open is called
        /// </summary>
        protected virtual INntpSocket CreateSocket()
        {
            return new NntpSocket(Address, Port, IsSecuredConnection)
            {
                ConnectionTimeout = ConnectionTimeout,
            };
        }

        /// <summary>
        ///  Helpers for validating if the socket is connected, throws an exception if not
        /// </summary>
        protected virtual void ValidateIsOpen()
        {
            if (!IsOpen)
            {
                throw new InvalidOperationException(Resource.NntpClient_NotOpen);
            }
        }

        /// <summary>
        ///  Helpers for validating if the socket is disconnected, throws an exception if not
        /// </summary>
        protected virtual void ValidateIsClosed()
        {
            if (IsOpen)
            {
                throw new InvalidOperationException(Resource.NntpClient_NotClosed);
            }
        }

        protected virtual Stream BuildReadStreamChain()
        {
            Stream baseStream = Socket.TransportStream;
            baseStream = new NonClosableStream(baseStream);

            return ReadStreamChainBuilder.Build(baseStream);
        }

        protected virtual Stream BuildWriteStreamChain()
        {
            Stream baseStream = Socket.TransportStream;
            baseStream = new NonClosableStream(baseStream);

            return WriteStreamChainBuilder.Build(baseStream);
        }

        protected virtual string ReadString()
        {
            using (NntpStreamReader reader = new NntpStreamReader(BuildReadStreamChain(), TextEncoding))
            {
                return reader.ReadToEnd();
            }
        }

        #endregion // Protected Methods

        #region Helpers

        private void Connect()
        {
            Socket.Connect();

            // Connect should answer with 1 response message
            ResponseMessage message = ReadResponseMessage();
            Rfc977ResponseCode responseCode = (Rfc977ResponseCode)message.ParseResponseCode();

            switch (responseCode)
            {
                case Rfc977ResponseCode.ServerReadyNoPostingAllowed:
                    IsPostingAllowed = false;
                    break;
                case Rfc977ResponseCode.ServerReadyPostingAllowed:
                    IsPostingAllowed = true;
                    break;
                default:
                    // unknown response code, throw exception
                    throw new NntpException(message);
            }
        }

        private void Authenticate()
        {
            WriteCommand("AUTHINFO USER " + Authentication.UserName);
            ResponseMessage message = ReadResponseMessage();
            Rfc4643ResponseCode responseCode = (Rfc4643ResponseCode)message.ParseResponseCode();

            if (responseCode == Rfc4643ResponseCode.PasswordRequired)
            {
                WriteCommand("AUTHINFO PASS " + Authentication.Password);
                message = ReadResponseMessage();
                responseCode = (Rfc4643ResponseCode)message.ParseResponseCode();

                if (responseCode != Rfc4643ResponseCode.AuthenticationAccepted)
                {
                    throw new NntpNotAuthorizedException(Resource.NntpClient_PasswordNotExcepted);
                }
            }
            else
            {
                throw new NntpNotAuthorizedException(Resource.NntpClient_AuthenticationFailed);
            }
        }

        private void Disconnect()
        {
            Socket.Disconnect();
        }

        #endregion // Helpers
    }
}
